In this example, we are going to learn about an alternative method to encode text data known as word embeddings. This is an incomplete tutorial on word embeddings but will at least give you the basic understanding on when and why we use them.

Learning objectives:

Requirements

# Initialize package
library(keras)
library(fs)
library(tidyverse)
library(glue)
library(progress)

# helper functions we'll use to explore word embeddings
source("helper_functions.R")

The “real” IMDB dataset

So far, we’ve been using the built-in IMBD dataset. Here, we are going to use the original data files which can be found at http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz. You can find the download instructions here. For those in the workshop we have already downloaded this data for you.

imdb_dir <- here::here("materials", "data", "imdb")
fs::dir_tree(imdb_dir, type = "directory")
/Users/b294776/Desktop/Workspace/Training/rstudio-conf-2020/dl-keras-tf/materials/data/imdb
├── test
│   ├── neg
│   └── pos
└── train
    ├── neg
    └── pos

You can see the data have already been separated into test vs training sets and positive vs negative sets. The actual reviews are contained in individual .txt files. We can use this structure to our advantage - the below iterates over each review and

  1. creates the path to each individual review file,
  2. creats a label based on the “neg” or “pos” folder the review is in,
  3. and saves the output as a data frame with each review on an individual row.
training_files <- file.path(imdb_dir, "train") %>%
  dir_ls() %>%
  map(dir_ls) %>%
  set_names(basename) %>%
  plyr::ldply(data_frame) %>%
  set_names(c("label", "path"))

training_files

We can see our response observations are balanced:

count(training_files, label)

We can now iterate over each row and

  1. save the label in a label vector,
  2. import the movie review and
  3. save in a texts vector.
obs <- nrow(training_files)
labels <- vector(mode = "integer", length = obs)
texts <- vector(mode = "character", length = obs)

# this just allows us to track progress of our loop
pb <- progress_bar$new(total = obs, width = 60)

for (file in seq_len(obs)) {
  pb$tick()
  
  label <- training_files[[file, "label"]]
  path <- training_files[[file, "path"]]
  
  labels[file] <- ifelse(label == "neg", 0, 1)
  texts[file] <- readChar(path, nchars = file.size(path)) 
  
}

We now have two vectors, one consisting of the labels and…

table(labels)
labels
    0     1 
12500 12500 

the other holding each review.

texts[1]
[1] "Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."

Exploratory text analysis

A little exploratory analysis will show us the total number of unique words across our corpus and the average length of each review.

text_df <- texts %>%
  tibble(.name_repair = ~ "text") %>%
  mutate(text_length = str_count(text, "\\w+"))

unique_words <- text_df %>%
  tidytext::unnest_tokens(word, text) %>%
  pull(word) %>%
  n_distinct()

avg_review_length <- median(text_df$text_length, na.rm = TRUE)
  
ggplot(text_df, aes(text_length)) +
  geom_histogram(bins = 100, fill = "grey70", color = "grey40") +
  geom_vline(xintercept = avg_review_length, color = "red", lty = "dashed") +
  scale_x_log10("# words") +
  ggtitle(glue("Median review length is {avg_review_length} words"),
          subtitle = glue("Total number of unique words is {unique_words}"))

Word embeddings for language modeling

Word embeddings are designed to encode general semantic relationships which can serve two principle purposes. The first is for language modeling which aims to encode words for the purpose of predicting synonyms, sentence completion, and word relationships. ℹ️

Although we are not focusing on word embeddings for this purpose, I have written a couple helper functions to train word embeddings for this purpose. See the code behind these helper functions here.

# clean up text and compute word embeddings
clean_text <- tolower(texts) %>%
  str_replace_all(pattern = "[[:punct:] ]+", replacement = " ") %>%
  str_trim()

word_embeddings <- get_embeddings(clean_text)
Creating vocabulary...
Creating term-co-occurence matrix...
Computing embeddings based on GloVe algorithm...
INFO [2019-12-09 09:14:04] 2019-12-09 09:14:04 - epoch 1, expected cost 0.0821
INFO [2019-12-09 09:14:05] 2019-12-09 09:14:05 - epoch 2, expected cost 0.0555
INFO [2019-12-09 09:14:06] 2019-12-09 09:14:06 - epoch 3, expected cost 0.0485
INFO [2019-12-09 09:14:07] 2019-12-09 09:14:07 - epoch 4, expected cost 0.0443
INFO [2019-12-09 09:14:08] 2019-12-09 09:14:08 - epoch 5, expected cost 0.0415
INFO [2019-12-09 09:14:09] 2019-12-09 09:14:09 - epoch 6, expected cost 0.0395
INFO [2019-12-09 09:14:11] 2019-12-09 09:14:11 - epoch 7, expected cost 0.0379
INFO [2019-12-09 09:14:12] 2019-12-09 09:14:12 - epoch 8, expected cost 0.0367
INFO [2019-12-09 09:14:13] 2019-12-09 09:14:13 - epoch 9, expected cost 0.0357
INFO [2019-12-09 09:14:14] 2019-12-09 09:14:14 - epoch 10, expected cost 0.0348
INFO [2019-12-09 09:14:15] 2019-12-09 09:14:15 - epoch 11, expected cost 0.0341
INFO [2019-12-09 09:14:16] 2019-12-09 09:14:16 - epoch 12, expected cost 0.0335
INFO [2019-12-09 09:14:17] 2019-12-09 09:14:17 - epoch 13, expected cost 0.0330
INFO [2019-12-09 09:14:18] 2019-12-09 09:14:18 - epoch 14, expected cost 0.0326
INFO [2019-12-09 09:14:19] 2019-12-09 09:14:19 - epoch 15, expected cost 0.0322
INFO [2019-12-09 09:14:20] 2019-12-09 09:14:20 - epoch 16, expected cost 0.0318
INFO [2019-12-09 09:14:22] 2019-12-09 09:14:22 - epoch 17, expected cost 0.0315
INFO [2019-12-09 09:14:23] 2019-12-09 09:14:23 - epoch 18, expected cost 0.0312
INFO [2019-12-09 09:14:23] Success: early stopping. Improvement at iterartion 18 is less then convergence_tol

Explore your own words!

# find words with similar embeddings
get_similar_words("horrible", word_embeddings)
 horrible  terrible     awful       bad    acting 
1.0000000 0.9132471 0.8663343 0.8041792 0.7790165 

Word embeddings for classification

The other principle purpose for word embeddings is to encode text for classification reasons. In this case, we train the word embeddings to take on weights that optimize the classification loss function. ℹ️

Prepare data

To prepare our data we need to convert or labels vector to a tensor:

labels <- as.array(labels)

But more importantly, we need to preprocess our text features. To do so we:

  1. Specify how many words we want to include. This will capture the 10,000 words with the highest usage (frequency).
  2. Create a text_tokenizer object which defines how we want to preprocess the text (i.e. convert to lowercase, remove punctuation, token splitting characters). For the most part, the defaults are sufficient.
  3. Apply the tokenizer to our text with fit_text_tokenizer. This results in an object with many details of our corpus (i.e. word counts, word index).
top_n_words <- 10000

tokenizer <- text_tokenizer(num_words = top_n_words) %>% 
  fit_text_tokenizer(texts)

names(tokenizer)
 [1] "char_level"                   "document_count"              
 [3] "filters"                      "fit_on_sequences"            
 [5] "fit_on_texts"                 "get_config"                  
 [7] "index_docs"                   "index_word"                  
 [9] "lower"                        "num_words"                   
[11] "oov_token"                    "sequences_to_matrix"         
[13] "sequences_to_texts"           "sequences_to_texts_generator"
[15] "split"                        "texts_to_matrix"             
[17] "texts_to_sequences"           "texts_to_sequences_generator"
[19] "to_json"                      "word_counts"                 
[21] "word_docs"                    "word_index"                  
total_word_index <- tokenizer$word_index
num_words_used <- tokenizer$num_words

glue("We have now tokenized our reviews. ", "We are considering {num_words_used} ",
     "of {length(total_word_index)} total unique words. The most common words ",
     "include:")
We have now tokenized our reviews. We are considering 10000 of 88582 total unique words. The most common words include:
head(total_word_index)
$the
[1] 1

$and
[1] 2

$a
[1] 3

$of
[1] 4

$to
[1] 5

$is
[1] 6

Next, we extract our vectorized review data as a list. This looks familiar from the earlier modules.

sequences <- texts_to_sequences(tokenizer, texts)

# The vectorized first instance:
sequences[[1]]
  [1]   62    4    3  129   34   44 7576 1414   15    3 4252  514   43   16    3  633
 [17]  133   12    6    3 1301  459    4 1751  209    3 7693  308    6  676   80   32
 [33] 2137 1110 3008   31    1  929    4   42 5120  469    9 2665 1751    1  223   55
 [49]   16   54  828 1318  847  228    9   40   96  122 1484   57  145   36    1  996
 [65]  141   27  676  122    1  411   59   94 2278  303  772    5    3  837   20    3
 [81] 1755  646   42  125   71   22  235  101   16   46   49  624   31  702   84  702
 [97]  378 3493    2 8422   67   27  107 3348

We can see how our tokenizer converted our original text to a cleaned up version:

cat("Original text:\n")
Original text:
texts[[1]]
[1] "Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."
cat("\nRevised text:\n")

Revised text:
paste(unlist(tokenizer$index_word)[sequences[[1]]] , collapse = " ")
[1] "story of a man who has unnatural feelings for a pig starts out with a opening scene that is a terrific example of absurd comedy a orchestra audience is turned into an insane violent mob by the crazy of it's singers unfortunately it stays absurd the whole time with no general narrative eventually making it just too off putting even those from the era should be turned off the dialogue would make shakespeare seem easy to a third on a technical level it's better than you might think with some good cinematography by future great future stars sally and forrest can be seen briefly"

Next, since each review is a different length, we need to limit ourselves to a certain number of words so that all our features (reviews) are the same length.

Note (?pad_sequences): * Any reviews that are shorter than this length will be padded. * Any reviews that are longer than this length will be truncated.

max_len <- 150
features <- pad_sequences(sequences, maxlen = max_len)
features[1,]
  [1]    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0
 [18]    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0
 [35]    0    0    0    0    0    0    0    0    0    0    0    0   62    4    3  129   34
 [52]   44 7576 1414   15    3 4252  514   43   16    3  633  133   12    6    3 1301  459
 [69]    4 1751  209    3 7693  308    6  676   80   32 2137 1110 3008   31    1  929    4
 [86]   42 5120  469    9 2665 1751    1  223   55   16   54  828 1318  847  228    9   40
[103]   96  122 1484   57  145   36    1  996  141   27  676  122    1  411   59   94 2278
[120]  303  772    5    3  837   20    3 1755  646   42  125   71   22  235  101   16   46
[137]   49  624   31  702   84  702  378 3493    2 8422   67   27  107 3348
paste(unlist(tokenizer$index_word)[features[1,]], collapse = " ")
[1] "story of a man who has unnatural feelings for a pig starts out with a opening scene that is a terrific example of absurd comedy a orchestra audience is turned into an insane violent mob by the crazy of it's singers unfortunately it stays absurd the whole time with no general narrative eventually making it just too off putting even those from the era should be turned off the dialogue would make shakespeare seem easy to a third on a technical level it's better than you might think with some good cinematography by future great future stars sally and forrest can be seen briefly"

Your Turn!

Check out different reviews and see how we have transformed the data. Remove eval=FALSE to run.

# use review number (i.e. 2, 10, 150)
which_review <- ____
  
cat(crayon::blue("Original text:\n"))
texts[[which_review ]]

cat(crayon::blue("\nRevised text:\n"))
paste(unlist(tokenizer$index_word)[features[which_review ,]] , collapse = " ")

cat(crayon::blue("\nEncoded text:\n"))
features[which_review ,]

Our data is now preprocessed! We have 25000 observations and 150 features. Our features data is a matrix where each row is a single observation and each column represents the words in the review in the order that they appear.

dim(features)
[1] 25000   150
dim(labels)
[1] 25000

Model training

To train our model we will use the validation_split procedure within fit. Remember, this takes the last XX% of our data to be used as our validation set. But if you recall, our data was organized in neg and pos folders so we should randomize our data to make sure our validation set doesn’t end up being all positive or negative reviews!

set.seed(123)
index <- sample(1:nrow(features))

x_train <- features[index, ]
y_train <- labels[index]

To create our network architecture that includes word embeddings, need to include two things:

  1. layer_embedding layer that creates the embeddings,
  2. layer_flatten to flatten our embeddings to a 2D tensor for our densely connected portion of our model
model <- keras_model_sequential() %>%
  layer_embedding(
    input_dim = top_n_words,   # number of words we are considering
    input_length = max_len,    # length that we have set each review to
    output_dim = 32            # length of our word embeddings
    ) %>%  
  layer_flatten() %>% 
  layer_dense(units = 1, activation = "sigmoid")

summary(model)
Model: "sequential_3"
_____________________________________________________________________________________________
Layer (type)                             Output Shape                          Param #       
=============================================================================================
embedding_1 (Embedding)                  (None, 150, 32)                       320000        
_____________________________________________________________________________________________
flatten_1 (Flatten)                      (None, 4800)                          0             
_____________________________________________________________________________________________
dense_1 (Dense)                          (None, 1)                             4801          
=============================================================================================
Total params: 324,801
Trainable params: 324,801
Non-trainable params: 0
_____________________________________________________________________________________________

The rest of our modeling procedures follows the same protocols that you’ve seen in the other modules.

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = "accuracy"
)

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)
best_epoch <- which.min(history$metrics$val_loss)
best_loss <- history$metrics$val_loss[best_epoch] %>% round(3)
best_acc <- history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("Our optimal loss is {best_loss} with an accuracy of {best_acc}")
Our optimal loss is 0.3 with an accuracy of 0.874
plot(history)

YOUR TURN! (5min)

Spend a few minutes adjusting this model and see how it impact performance. You may want to test:

  • Does increasing and decreasing the word embedding dimension (output_dim) impacts performance?
  • How does the learning rate impact performance?
  • You may have noticed that we didn’t add any additional hidden layers to the densely connected portion of our model. Does adding 1 or 2 more hidden layers improve performance?
yourturn_model <- keras_model_sequential() %>%
  layer_embedding(
    input_dim = _____,  
    input_length = _____,   
    output_dim = _____           
    ) %>%  
  layer_flatten() %>% 
  layer_dense(units = ____, activation = ____) %>%
  layer_dense(units = 1, activation = "sigmoid")

yourturn_model %>% compile(
  optimizer = _____,
  loss = "binary_crossentropy",
  metrics = "accuracy"
)

yourturn_results <- yourturn_model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

Comparing embeddings

Recall that the word embeddings we found for natural language modeling created results like:

# natural language modeling embeddings
get_similar_words("horrible", word_embeddings)
 horrible  terrible     awful       bad    acting 
1.0000000 0.9132471 0.8663343 0.8041792 0.7790165 

However, embeddings we find for classification tasks are not always so clean and intuitive. We can get the word embeddings from our classification model with:

wts <- get_weights(model)
embedding_wts <- wts[[1]]

The following just does some bookkeeping to extract the applicable words and assign them as row names to the embedding matrix.

words <- tokenizer$word_index %>% 
  as_tibble() %>% 
  pivot_longer(everything(), names_to = "word", values_to = "id") %>%
  filter(id <= tokenizer$num_words) %>%
  arrange(id)

row.names(embedding_wts) <- words$word

The following is one of the custom functions you imported from the helper_functions.R file. You can see the word embeddings that most closely align to a given word are not as intuitive as those produced from the natural language model. However, these are the embeddings that optimized for the classification procedure at hand.

similar_classification_words("horrible", embedding_wts)
 horrible      foul    source    rivals  homicide      fits 
1.0000000 0.7439281 0.7299364 0.7215308 0.7141167 0.7128572 

Here’s a handy sequence of code that uses the t-SNE methodology to visualize nearest neighbor word embeddings.

# plotting too many words makes the output hard to read
n_words_to_plot <- 1000

tsne <- Rtsne::Rtsne(
  X = embedding_wts[1:n_words_to_plot,], 
  perplexity = 100, 
  pca = FALSE
  )

p <- tsne$Y %>%
  as.data.frame() %>%
  mutate(word = row.names(embedding_wts)[1:n_words_to_plot]) %>%
  ggplot(aes(x = V1, y = V2, label = word)) + 
  geom_text(size = 3)

plotly::ggplotly(p)
LS0tCnRpdGxlOiAiTkxQOiBXb3JkIGVtYmVkZGluZ3MiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCmBgYAoKSW4gdGhpcyBleGFtcGxlLCB3ZSBhcmUgZ29pbmcgdG8gbGVhcm4gYWJvdXQgYW4gYWx0ZXJuYXRpdmUgbWV0aG9kIHRvIGVuY29kZSAKdGV4dCBkYXRhIGtub3duIGFzIF9fX3dvcmQgZW1iZWRkaW5nc19fXy4gVGhpcyBpcyBhbiBpbmNvbXBsZXRlIHR1dG9yaWFsIG9uIAp3b3JkIGVtYmVkZGluZ3MgYnV0IHdpbGwgYXQgbGVhc3QgZ2l2ZSB5b3UgdGhlIGJhc2ljIHVuZGVyc3RhbmRpbmcgb24gd2hlbiBhbmQgCndoeSB3ZSB1c2UgdGhlbS4KCkxlYXJuaW5nIG9iamVjdGl2ZXM6CgotIFdoYXQgd29yZCBlbWJlZGRpbmdzIGFyZS4KLSBUaGUgdHdvIG1haW4gY29udGV4dHMgdGhhdCB3b3JkIGVtYmVkZGluZ3MgYXJlIHRyYWluZWQuCi0gV2hlbiB3ZSBzaG91bGQgdXNlIHdvcmQgZW1iZWRkaW5ncy4KLSBIb3cgdG8gdHJhaW4gd29yZCBlbWJlZGRpbmdzIGZvciBjbGFzc2lmaWNhdGlvbiBwdXJwb3Nlcy4KCiMgUmVxdWlyZW1lbnRzCgpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KIyBJbml0aWFsaXplIHBhY2thZ2UKbGlicmFyeShrZXJhcykKbGlicmFyeShmcykKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoZ2x1ZSkKbGlicmFyeShwcm9ncmVzcykKCiMgaGVscGVyIGZ1bmN0aW9ucyB3ZSdsbCB1c2UgdG8gZXhwbG9yZSB3b3JkIGVtYmVkZGluZ3MKc291cmNlKCJoZWxwZXJfZnVuY3Rpb25zLlIiKQpgYGAKCiMgVGhlICJyZWFsIiBJTURCIGRhdGFzZXQKClNvIGZhciwgd2UndmUgYmVlbiB1c2luZyB0aGUgYnVpbHQtaW4gSU1CRCBkYXRhc2V0LiBIZXJlLCB3ZSBhcmUgZ29pbmcgdG8gdXNlIAp0aGUgb3JpZ2luYWwgZGF0YSBmaWxlcyB3aGljaCBjYW4gYmUgZm91bmQgYXQKaHR0cDovL2FpLnN0YW5mb3JkLmVkdS9+YW1hYXMvZGF0YS9zZW50aW1lbnQvYWNsSW1kYl92MS50YXIuZ3ouIFlvdSBjYW4gZmluZCB0aGUKZG93bmxvYWQgaW5zdHJ1Y3Rpb25zIFtoZXJlXShodHRwOi8vYml0Lmx5L2RsLXJxbXRzKS4gRm9yIHRob3NlIGluIHRoZSB3b3Jrc2hvcAp3ZSBoYXZlIGFscmVhZHkgZG93bmxvYWRlZCB0aGlzIGRhdGEgZm9yIHlvdS4KCmBgYHtyfQppbWRiX2RpciA8LSBoZXJlOjpoZXJlKCJtYXRlcmlhbHMiLCAiZGF0YSIsICJpbWRiIikKZnM6OmRpcl90cmVlKGltZGJfZGlyLCB0eXBlID0gImRpcmVjdG9yeSIpCmBgYAoKWW91IGNhbiBzZWUgdGhlIGRhdGEgaGF2ZSBhbHJlYWR5IGJlZW4gc2VwYXJhdGVkIGludG8gdGVzdCB2cyB0cmFpbmluZyBzZXRzIGFuZCAKcG9zaXRpdmUgdnMgbmVnYXRpdmUgc2V0cy4gVGhlIGFjdHVhbCByZXZpZXdzIGFyZSBjb250YWluZWQgaW4gaW5kaXZpZHVhbCAudHh0CmZpbGVzLiBXZSBjYW4gdXNlIHRoaXMgc3RydWN0dXJlIHRvIG91ciBhZHZhbnRhZ2UgLSB0aGUgYmVsb3cgaXRlcmF0ZXMgb3ZlciBlYWNoCnJldmlldyBhbmQgCgoxLiBjcmVhdGVzIHRoZSBwYXRoIHRvIGVhY2ggaW5kaXZpZHVhbCByZXZpZXcgZmlsZSwKMi4gY3JlYXRzIGEgbGFiZWwgYmFzZWQgb24gdGhlICJuZWciIG9yICJwb3MiIGZvbGRlciB0aGUgcmV2aWV3IGlzIGluLAozLiBhbmQgc2F2ZXMgdGhlIG91dHB1dCBhcyBhIGRhdGEgZnJhbWUgd2l0aCBlYWNoIHJldmlldyBvbiBhbiBpbmRpdmlkdWFsIHJvdy4KCmBgYHtyfQp0cmFpbmluZ19maWxlcyA8LSBmaWxlLnBhdGgoaW1kYl9kaXIsICJ0cmFpbiIpICU+JQogIGRpcl9scygpICU+JQogIG1hcChkaXJfbHMpICU+JQogIHNldF9uYW1lcyhiYXNlbmFtZSkgJT4lCiAgcGx5cjo6bGRwbHkoZGF0YV9mcmFtZSkgJT4lCiAgc2V0X25hbWVzKGMoImxhYmVsIiwgInBhdGgiKSkKCnRyYWluaW5nX2ZpbGVzCmBgYAoKV2UgY2FuIHNlZSBvdXIgcmVzcG9uc2Ugb2JzZXJ2YXRpb25zIGFyZSBiYWxhbmNlZDoKCmBgYHtyfQpjb3VudCh0cmFpbmluZ19maWxlcywgbGFiZWwpCmBgYAoKV2UgY2FuIG5vdyBpdGVyYXRlIG92ZXIgZWFjaCByb3cgYW5kCgoxLiBzYXZlIHRoZSBsYWJlbCBpbiBhIGxhYmVsIHZlY3RvciwKMi4gaW1wb3J0IHRoZSBtb3ZpZSByZXZpZXcgYW5kCjMuIHNhdmUgaW4gYSB0ZXh0cyB2ZWN0b3IuCgpgYGB7cn0Kb2JzIDwtIG5yb3codHJhaW5pbmdfZmlsZXMpCmxhYmVscyA8LSB2ZWN0b3IobW9kZSA9ICJpbnRlZ2VyIiwgbGVuZ3RoID0gb2JzKQp0ZXh0cyA8LSB2ZWN0b3IobW9kZSA9ICJjaGFyYWN0ZXIiLCBsZW5ndGggPSBvYnMpCgojIHRoaXMganVzdCBhbGxvd3MgdXMgdG8gdHJhY2sgcHJvZ3Jlc3Mgb2Ygb3VyIGxvb3AKcGIgPC0gcHJvZ3Jlc3NfYmFyJG5ldyh0b3RhbCA9IG9icywgd2lkdGggPSA2MCkKCmZvciAoZmlsZSBpbiBzZXFfbGVuKG9icykpIHsKICBwYiR0aWNrKCkKICAKICBsYWJlbCA8LSB0cmFpbmluZ19maWxlc1tbZmlsZSwgImxhYmVsIl1dCiAgcGF0aCA8LSB0cmFpbmluZ19maWxlc1tbZmlsZSwgInBhdGgiXV0KICAKICBsYWJlbHNbZmlsZV0gPC0gaWZlbHNlKGxhYmVsID09ICJuZWciLCAwLCAxKQogIHRleHRzW2ZpbGVdIDwtIHJlYWRDaGFyKHBhdGgsIG5jaGFycyA9IGZpbGUuc2l6ZShwYXRoKSkgCiAgCn0KYGBgCgpXZSBub3cgaGF2ZSB0d28gdmVjdG9ycywgb25lIGNvbnNpc3Rpbmcgb2YgdGhlIGxhYmVscyBhbmQuLi4KCmBgYHtyfQp0YWJsZShsYWJlbHMpCmBgYAoKdGhlIG90aGVyIGhvbGRpbmcgZWFjaCByZXZpZXcuCgpgYGB7cn0KdGV4dHNbMV0KYGBgCgojIEV4cGxvcmF0b3J5IHRleHQgYW5hbHlzaXMKCkEgbGl0dGxlIGV4cGxvcmF0b3J5IGFuYWx5c2lzIHdpbGwgc2hvdyB1cyB0aGUgdG90YWwgbnVtYmVyIG9mIHVuaXF1ZSB3b3JkcwphY3Jvc3Mgb3VyIGNvcnB1cyBhbmQgdGhlIGF2ZXJhZ2UgbGVuZ3RoIG9mIGVhY2ggcmV2aWV3LgoKYGBge3IsIGZpZy5oZWlnaHQ9My41fQp0ZXh0X2RmIDwtIHRleHRzICU+JQogIHRpYmJsZSgubmFtZV9yZXBhaXIgPSB+ICJ0ZXh0IikgJT4lCiAgbXV0YXRlKHRleHRfbGVuZ3RoID0gc3RyX2NvdW50KHRleHQsICJcXHcrIikpCgp1bmlxdWVfd29yZHMgPC0gdGV4dF9kZiAlPiUKICB0aWR5dGV4dDo6dW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0KSAlPiUKICBwdWxsKHdvcmQpICU+JQogIG5fZGlzdGluY3QoKQoKYXZnX3Jldmlld19sZW5ndGggPC0gbWVkaWFuKHRleHRfZGYkdGV4dF9sZW5ndGgsIG5hLnJtID0gVFJVRSkKICAKZ2dwbG90KHRleHRfZGYsIGFlcyh0ZXh0X2xlbmd0aCkpICsKICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gMTAwLCBmaWxsID0gImdyZXk3MCIsIGNvbG9yID0gImdyZXk0MCIpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSBhdmdfcmV2aWV3X2xlbmd0aCwgY29sb3IgPSAicmVkIiwgbHR5ID0gImRhc2hlZCIpICsKICBzY2FsZV94X2xvZzEwKCIjIHdvcmRzIikgKwogIGdndGl0bGUoZ2x1ZSgiTWVkaWFuIHJldmlldyBsZW5ndGggaXMge2F2Z19yZXZpZXdfbGVuZ3RofSB3b3JkcyIpLAogICAgICAgICAgc3VidGl0bGUgPSBnbHVlKCJUb3RhbCBudW1iZXIgb2YgdW5pcXVlIHdvcmRzIGlzIHt1bmlxdWVfd29yZHN9IikpCmBgYAoKCiMgV29yZCBlbWJlZGRpbmdzIGZvciBsYW5ndWFnZSBtb2RlbGluZwoKV29yZCBlbWJlZGRpbmdzIGFyZSBkZXNpZ25lZCB0byBlbmNvZGUgZ2VuZXJhbCBzZW1hbnRpYyByZWxhdGlvbnNoaXBzIHdoaWNoIGNhbgpzZXJ2ZSB0d28gcHJpbmNpcGxlIHB1cnBvc2VzLiBUaGUgZmlyc3QgaXMgZm9yIF9fX2xhbmd1YWdlIG1vZGVsaW5nX19fIHdoaWNoIAphaW1zIHRvIGVuY29kZSB3b3JkcyBmb3IgdGhlIHB1cnBvc2Ugb2YgcHJlZGljdGluZyBzeW5vbnltcywgc2VudGVuY2UgY29tcGxldGlvbiwgCmFuZCB3b3JkIHJlbGF0aW9uc2hpcHMuIFvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtMDYpCgpBbHRob3VnaCB3ZSBhcmUgbm90IGZvY3VzaW5nIG9uIHdvcmQgZW1iZWRkaW5ncyBmb3IgdGhpcyBwdXJwb3NlLCBJIGhhdmUgd3JpdHRlbgphIGNvdXBsZSBoZWxwZXIgZnVuY3Rpb25zIHRvIHRyYWluIHdvcmQgZW1iZWRkaW5ncyBmb3IgdGhpcyBwdXJwb3NlLiBTZWUgdGhlCmNvZGUgYmVoaW5kIHRoZXNlIGhlbHBlciBmdW5jdGlvbnMgW2hlcmVdKGh0dHBzOi8vYml0Lmx5LzMySENQMUcpLgoKYGBge3J9CiMgY2xlYW4gdXAgdGV4dCBhbmQgY29tcHV0ZSB3b3JkIGVtYmVkZGluZ3MKY2xlYW5fdGV4dCA8LSB0b2xvd2VyKHRleHRzKSAlPiUKICBzdHJfcmVwbGFjZV9hbGwocGF0dGVybiA9ICJbWzpwdW5jdDpdIF0rIiwgcmVwbGFjZW1lbnQgPSAiICIpICU+JQogIHN0cl90cmltKCkKCndvcmRfZW1iZWRkaW5ncyA8LSBnZXRfZW1iZWRkaW5ncyhjbGVhbl90ZXh0KQpgYGAKCkV4cGxvcmUgeW91ciBvd24gd29yZHMhCgpgYGB7cn0KIyBmaW5kIHdvcmRzIHdpdGggc2ltaWxhciBlbWJlZGRpbmdzCmdldF9zaW1pbGFyX3dvcmRzKCJob3JyaWJsZSIsIHdvcmRfZW1iZWRkaW5ncykKYGBgCgoKIyBXb3JkIGVtYmVkZGluZ3MgZm9yIGNsYXNzaWZpY2F0aW9uCgpUaGUgb3RoZXIgcHJpbmNpcGxlIHB1cnBvc2UgZm9yIHdvcmQgZW1iZWRkaW5ncyBpcyB0byBlbmNvZGUgdGV4dCBmb3IgCmNsYXNzaWZpY2F0aW9uIHJlYXNvbnMuIEluIHRoaXMgY2FzZSwgd2UgdHJhaW4gdGhlIHdvcmQgZW1iZWRkaW5ncyB0byB0YWtlIG9uIAp3ZWlnaHRzIHRoYXQgb3B0aW1pemUgdGhlIGNsYXNzaWZpY2F0aW9uIGxvc3MgZnVuY3Rpb24uIFvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtMDYjMTIpCgojIyBQcmVwYXJlIGRhdGEKClRvIHByZXBhcmUgb3VyIGRhdGEgd2UgbmVlZCB0byBjb252ZXJ0IG9yIGBsYWJlbHNgIHZlY3RvciB0byBhIHRlbnNvcjoKCmBgYHtyfQpsYWJlbHMgPC0gYXMuYXJyYXkobGFiZWxzKQpgYGAKCkJ1dCBtb3JlIGltcG9ydGFudGx5LCB3ZSBuZWVkIHRvIHByZXByb2Nlc3Mgb3VyIHRleHQgZmVhdHVyZXMuIFRvIGRvIHNvIHdlOgoKMS4gU3BlY2lmeSBob3cgbWFueSB3b3JkcyB3ZSB3YW50IHRvIGluY2x1ZGUuIFRoaXMgd2lsbCBjYXB0dXJlIHRoZSAxMCwwMDAgd29yZHMKICAgd2l0aCB0aGUgaGlnaGVzdCB1c2FnZSAoZnJlcXVlbmN5KS4KMi4gQ3JlYXRlIGEgYHRleHRfdG9rZW5pemVyYCBvYmplY3Qgd2hpY2ggZGVmaW5lcyBob3cgd2Ugd2FudCB0byBwcmVwcm9jZXNzIHRoZQogICB0ZXh0IChpLmUuIGNvbnZlcnQgdG8gbG93ZXJjYXNlLCByZW1vdmUgcHVuY3R1YXRpb24sIHRva2VuIHNwbGl0dGluZyAKICAgY2hhcmFjdGVycykuIEZvciB0aGUgbW9zdCBwYXJ0LCB0aGUgZGVmYXVsdHMgYXJlIHN1ZmZpY2llbnQuCjMuIEFwcGx5IHRoZSB0b2tlbml6ZXIgdG8gb3VyIHRleHQgd2l0aCBgZml0X3RleHRfdG9rZW5pemVyYC4gVGhpcyByZXN1bHRzIGluIGFuCiAgIG9iamVjdCB3aXRoIG1hbnkgZGV0YWlscyBvZiBvdXIgY29ycHVzIChpLmUuIHdvcmQgY291bnRzLCB3b3JkIGluZGV4KS4KCmBgYHtyfQp0b3Bfbl93b3JkcyA8LSAxMDAwMAoKdG9rZW5pemVyIDwtIHRleHRfdG9rZW5pemVyKG51bV93b3JkcyA9IHRvcF9uX3dvcmRzKSAlPiUgCiAgZml0X3RleHRfdG9rZW5pemVyKHRleHRzKQoKbmFtZXModG9rZW5pemVyKQpgYGAKCmBgYHtyfQp0b3RhbF93b3JkX2luZGV4IDwtIHRva2VuaXplciR3b3JkX2luZGV4Cm51bV93b3Jkc191c2VkIDwtIHRva2VuaXplciRudW1fd29yZHMKCmdsdWUoIldlIGhhdmUgbm93IHRva2VuaXplZCBvdXIgcmV2aWV3cy4gIiwgIldlIGFyZSBjb25zaWRlcmluZyB7bnVtX3dvcmRzX3VzZWR9ICIsCiAgICAgIm9mIHtsZW5ndGgodG90YWxfd29yZF9pbmRleCl9IHRvdGFsIHVuaXF1ZSB3b3Jkcy4gVGhlIG1vc3QgY29tbW9uIHdvcmRzICIsCiAgICAgImluY2x1ZGU6IikKaGVhZCh0b3RhbF93b3JkX2luZGV4KQpgYGAKCk5leHQsIHdlIGV4dHJhY3Qgb3VyIHZlY3Rvcml6ZWQgcmV2aWV3IGRhdGEgYXMgYSBsaXN0LiBUaGlzIGxvb2tzIGZhbWlsaWFyIGZyb20gCnRoZSBlYXJsaWVyIG1vZHVsZXMuCgpgYGB7cn0Kc2VxdWVuY2VzIDwtIHRleHRzX3RvX3NlcXVlbmNlcyh0b2tlbml6ZXIsIHRleHRzKQoKIyBUaGUgdmVjdG9yaXplZCBmaXJzdCBpbnN0YW5jZToKc2VxdWVuY2VzW1sxXV0KYGBgCgpXZSBjYW4gc2VlIGhvdyBvdXIgdG9rZW5pemVyIGNvbnZlcnRlZCBvdXIgb3JpZ2luYWwgdGV4dCB0byBhIGNsZWFuZWQgdXAgCnZlcnNpb246CgpgYGB7cn0gCmNhdChjcmF5b246OmJsdWUoIk9yaWdpbmFsIHRleHQ6XG4iKSkKdGV4dHNbWzFdXQoKY2F0KGNyYXlvbjo6Ymx1ZSgiXG5SZXZpc2VkIHRleHQ6XG4iKSkKcGFzdGUodW5saXN0KHRva2VuaXplciRpbmRleF93b3JkKVtzZXF1ZW5jZXNbWzFdXV0gLCBjb2xsYXBzZSA9ICIgIikKYGBgCgpOZXh0LCBzaW5jZSBlYWNoIHJldmlldyBpcyBhIGRpZmZlcmVudCBsZW5ndGgsIHdlIG5lZWQgdG8gbGltaXQgb3Vyc2VsdmVzIHRvIGEKY2VydGFpbiBudW1iZXIgb2Ygd29yZHMgc28gdGhhdCBhbGwgb3VyIGZlYXR1cmVzIChyZXZpZXdzKSBhcmUgdGhlIHNhbWUgbGVuZ3RoLiAKCk5vdGUgKGA/cGFkX3NlcXVlbmNlc2ApOgoqIEFueSByZXZpZXdzIHRoYXQgYXJlIHNob3J0ZXIgdGhhbiB0aGlzIGxlbmd0aCB3aWxsIGJlIHBhZGRlZC4KKiBBbnkgcmV2aWV3cyB0aGF0IGFyZSBsb25nZXIgdGhhbiB0aGlzIGxlbmd0aCB3aWxsIGJlIHRydW5jYXRlZC4KCmBgYHtyfQptYXhfbGVuIDwtIDE1MApmZWF0dXJlcyA8LSBwYWRfc2VxdWVuY2VzKHNlcXVlbmNlcywgbWF4bGVuID0gbWF4X2xlbikKYGBgCgpgYGB7cn0KZmVhdHVyZXNbMSxdCmBgYAoKYGBge3J9CnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbZmVhdHVyZXNbMSxdXSwgY29sbGFwc2UgPSAiICIpCmBgYAoKIyMjIFlvdXIgVHVybiEKCkNoZWNrIG91dCBkaWZmZXJlbnQgcmV2aWV3cyBhbmQgc2VlIGhvdyB3ZSBoYXZlIHRyYW5zZm9ybWVkIHRoZSBkYXRhLiBSZW1vdmUgCmBldmFsPUZBTFNFYCB0byBydW4uCgpgYGB7ciwgZXZhbD1GQUxTRX0KIyB1c2UgcmV2aWV3IG51bWJlciAoaS5lLiAyLCAxMCwgMTUwKQp3aGljaF9yZXZpZXcgPC0gX19fXwogIApjYXQoY3JheW9uOjpibHVlKCJPcmlnaW5hbCB0ZXh0OlxuIikpCnRleHRzW1t3aGljaF9yZXZpZXcgXV0KCmNhdChjcmF5b246OmJsdWUoIlxuUmV2aXNlZCB0ZXh0OlxuIikpCnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbZmVhdHVyZXNbd2hpY2hfcmV2aWV3ICxdXSAsIGNvbGxhcHNlID0gIiAiKQoKY2F0KGNyYXlvbjo6Ymx1ZSgiXG5FbmNvZGVkIHRleHQ6XG4iKSkKZmVhdHVyZXNbd2hpY2hfcmV2aWV3ICxdCmBgYAoKCk91ciBkYXRhIGlzIG5vdyBwcmVwcm9jZXNzZWQhIFdlIGhhdmUgYHIgbnJvdyhmZWF0dXJlcylgIG9ic2VydmF0aW9ucyBhbmQgCmByIG5jb2woZmVhdHVyZXMpYCBmZWF0dXJlcy4gT3VyIGBmZWF0dXJlc2AgZGF0YSBpcyBhIG1hdHJpeCB3aGVyZSBlYWNoIHJvdyBpcwphIHNpbmdsZSBvYnNlcnZhdGlvbiBhbmQgZWFjaCBjb2x1bW4gcmVwcmVzZW50cyB0aGUgd29yZHMgaW4gdGhlIHJldmlldyBpbiB0aGUKb3JkZXIgdGhhdCB0aGV5IGFwcGVhci4KCmBgYHtyfQpkaW0oZmVhdHVyZXMpCmRpbShsYWJlbHMpCmBgYAoKCiMjIE1vZGVsIHRyYWluaW5nCgpUbyB0cmFpbiBvdXIgbW9kZWwgd2Ugd2lsbCB1c2UgdGhlIGB2YWxpZGF0aW9uX3NwbGl0YCBwcm9jZWR1cmUgd2l0aGluIGBmaXRgLiAKUmVtZW1iZXIsIHRoaXMgdGFrZXMgdGhlIGxhc3QgWFglIG9mIG91ciBkYXRhIHRvIGJlIHVzZWQgYXMgb3VyIHZhbGlkYXRpb24gc2V0LiAKQnV0IGlmIHlvdSByZWNhbGwsIG91ciBkYXRhIHdhcyBvcmdhbml6ZWQgaW4gbmVnIGFuZCBwb3MgZm9sZGVycyBzbyB3ZSBzaG91bGQgCnJhbmRvbWl6ZSBvdXIgZGF0YSB0byBtYWtlIHN1cmUgb3VyIHZhbGlkYXRpb24gc2V0IGRvZXNuJ3QgZW5kIHVwIGJlaW5nIGFsbCAKcG9zaXRpdmUgb3IgbmVnYXRpdmUgcmV2aWV3cyEKCmBgYHtyfQpzZXQuc2VlZCgxMjMpCmluZGV4IDwtIHNhbXBsZSgxOm5yb3coZmVhdHVyZXMpKQoKeF90cmFpbiA8LSBmZWF0dXJlc1tpbmRleCwgXQp5X3RyYWluIDwtIGxhYmVsc1tpbmRleF0KYGBgCgpUbyBjcmVhdGUgb3VyIG5ldHdvcmsgYXJjaGl0ZWN0dXJlIHRoYXQgaW5jbHVkZXMgd29yZCBlbWJlZGRpbmdzLCBuZWVkIHRvIAppbmNsdWRlIHR3byB0aGluZ3M6CgoxLiBgbGF5ZXJfZW1iZWRkaW5nYCBsYXllciB0aGF0IGNyZWF0ZXMgdGhlIGVtYmVkZGluZ3MsCjIuIGBsYXllcl9mbGF0dGVuYCB0byBmbGF0dGVuIG91ciBlbWJlZGRpbmdzIHRvIGEgMkQgdGVuc29yIGZvciBvdXIgZGVuc2VseSAKICAgIGNvbm5lY3RlZCBwb3J0aW9uIG9mIG91ciBtb2RlbAoKYGBge3J9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9lbWJlZGRpbmcoCiAgICBpbnB1dF9kaW0gPSB0b3Bfbl93b3JkcywgICAjIG51bWJlciBvZiB3b3JkcyB3ZSBhcmUgY29uc2lkZXJpbmcKICAgIGlucHV0X2xlbmd0aCA9IG1heF9sZW4sICAgICMgbGVuZ3RoIHRoYXQgd2UgaGF2ZSBzZXQgZWFjaCByZXZpZXcgdG8KICAgIG91dHB1dF9kaW0gPSAzMiAgICAgICAgICAgICMgbGVuZ3RoIG9mIG91ciB3b3JkIGVtYmVkZGluZ3MKICAgICkgJT4lICAKICBsYXllcl9mbGF0dGVuKCkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikKCnN1bW1hcnkobW9kZWwpCmBgYAoKVGhlIHJlc3Qgb2Ygb3VyIG1vZGVsaW5nIHByb2NlZHVyZXMgZm9sbG93cyB0aGUgc2FtZSBwcm90b2NvbHMgdGhhdCB5b3UndmUgc2VlbiAKaW4gdGhlIG90aGVyIG1vZHVsZXMuCgpgYGB7cn0KbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLAogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSIsCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sIHlfdHJhaW4sCiAgZXBvY2hzID0gMTAsCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIKKQpgYGAKCmBgYHtyfQpiZXN0X2Vwb2NoIDwtIHdoaWNoLm1pbihoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MpCmJlc3RfbG9zcyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCmJlc3RfYWNjIDwtIGhpc3RvcnkkbWV0cmljcyR2YWxfYWNjdXJhY3lbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCgpnbHVlKCJPdXIgb3B0aW1hbCBsb3NzIGlzIHtiZXN0X2xvc3N9IHdpdGggYW4gYWNjdXJhY3kgb2Yge2Jlc3RfYWNjfSIpCmBgYAoKCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQpwbG90KGhpc3RvcnkpCmBgYAoKIyMgWU9VUiBUVVJOISAoNW1pbikKClNwZW5kIGEgZmV3IG1pbnV0ZXMgYWRqdXN0aW5nIHRoaXMgbW9kZWwgYW5kIHNlZSBob3cgaXQgaW1wYWN0IHBlcmZvcm1hbmNlLiBZb3UKbWF5IHdhbnQgdG8gdGVzdDoKCi0gRG9lcyBpbmNyZWFzaW5nIGFuZCBkZWNyZWFzaW5nIHRoZSB3b3JkIGVtYmVkZGluZyBkaW1lbnNpb24gKGBvdXRwdXRfZGltYCkKICBpbXBhY3RzIHBlcmZvcm1hbmNlPwotIEhvdyBkb2VzIHRoZSBsZWFybmluZyByYXRlIGltcGFjdCBwZXJmb3JtYW5jZT8KLSBZb3UgbWF5IGhhdmUgbm90aWNlZCB0aGF0IHdlIGRpZG4ndCBhZGQgYW55IGFkZGl0aW9uYWwgaGlkZGVuIGxheWVycyB0byB0aGUKICBkZW5zZWx5IGNvbm5lY3RlZCBwb3J0aW9uIG9mIG91ciBtb2RlbC4gIERvZXMgYWRkaW5nIDEgb3IgMiBtb3JlIGhpZGRlbiBsYXllcnMKICBpbXByb3ZlIHBlcmZvcm1hbmNlPwoKYGBge3IsIGV2YWw9RkFMU0V9CnlvdXJ0dXJuX21vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9lbWJlZGRpbmcoCiAgICBpbnB1dF9kaW0gPSBfX19fXywgIAogICAgaW5wdXRfbGVuZ3RoID0gX19fX18sICAgCiAgICBvdXRwdXRfZGltID0gX19fX18gICAgICAgICAgIAogICAgKSAlPiUgIAogIGxheWVyX2ZsYXR0ZW4oKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSBfX19fLCBhY3RpdmF0aW9uID0gX19fXykgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiKQoKeW91cnR1cm5fbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gX19fX18sCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICBtZXRyaWNzID0gImFjY3VyYWN5IgopCgp5b3VydHVybl9yZXN1bHRzIDwtIHlvdXJ0dXJuX21vZGVsICU+JSBmaXQoCiAgeF90cmFpbiwgeV90cmFpbiwKICBlcG9jaHMgPSAxMCwKICBiYXRjaF9zaXplID0gMzIsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMgopCmBgYAoKIyMgQ29tcGFyaW5nIGVtYmVkZGluZ3MKClJlY2FsbCB0aGF0IHRoZSB3b3JkIGVtYmVkZGluZ3Mgd2UgZm91bmQgZm9yIG5hdHVyYWwgbGFuZ3VhZ2UgbW9kZWxpbmcgY3JlYXRlZCAKcmVzdWx0cyBsaWtlOgoKYGBge3J9CiMgbmF0dXJhbCBsYW5ndWFnZSBtb2RlbGluZyBlbWJlZGRpbmdzCmdldF9zaW1pbGFyX3dvcmRzKCJob3JyaWJsZSIsIHdvcmRfZW1iZWRkaW5ncykKYGBgCgpIb3dldmVyLCBlbWJlZGRpbmdzIHdlIGZpbmQgZm9yIGNsYXNzaWZpY2F0aW9uIHRhc2tzIGFyZSBub3QgYWx3YXlzIHNvIGNsZWFuIGFuZCAKaW50dWl0aXZlLiBXZSBjYW4gZ2V0IHRoZSB3b3JkIGVtYmVkZGluZ3MgZnJvbSBvdXIgY2xhc3NpZmljYXRpb24gbW9kZWwgd2l0aDoKCmBgYHtyfQp3dHMgPC0gZ2V0X3dlaWdodHMobW9kZWwpCmVtYmVkZGluZ193dHMgPC0gd3RzW1sxXV0KYGBgCgpUaGUgZm9sbG93aW5nIGp1c3QgZG9lcyBzb21lIGJvb2trZWVwaW5nIHRvIGV4dHJhY3QgdGhlIGFwcGxpY2FibGUgd29yZHMgYW5kIAphc3NpZ24gdGhlbSBhcyByb3cgbmFtZXMgdG8gdGhlIGVtYmVkZGluZyBtYXRyaXguCgpgYGB7cn0Kd29yZHMgPC0gdG9rZW5pemVyJHdvcmRfaW5kZXggJT4lIAogIGFzX3RpYmJsZSgpICU+JSAKICBwaXZvdF9sb25nZXIoZXZlcnl0aGluZygpLCBuYW1lc190byA9ICJ3b3JkIiwgdmFsdWVzX3RvID0gImlkIikgJT4lCiAgZmlsdGVyKGlkIDw9IHRva2VuaXplciRudW1fd29yZHMpICU+JQogIGFycmFuZ2UoaWQpCgpyb3cubmFtZXMoZW1iZWRkaW5nX3d0cykgPC0gd29yZHMkd29yZApgYGAKClRoZSBmb2xsb3dpbmcgaXMgb25lIG9mIHRoZSBjdXN0b20gZnVuY3Rpb25zIHlvdSBpbXBvcnRlZCBmcm9tIHRoZSAKW2hlbHBlcl9mdW5jdGlvbnMuUl0oaHR0cHM6Ly9iaXQubHkvMzJIQ1AxRykgZmlsZS4gWW91IGNhbiBzZWUgdGhlIHdvcmQgCmVtYmVkZGluZ3MgdGhhdCBtb3N0IGNsb3NlbHkgYWxpZ24gdG8gYSBnaXZlbiB3b3JkIGFyZSBub3QgYXMgaW50dWl0aXZlIGFzIHRob3NlCnByb2R1Y2VkIGZyb20gdGhlIG5hdHVyYWwgbGFuZ3VhZ2UgbW9kZWwuIEhvd2V2ZXIsIHRoZXNlIGFyZSB0aGUgZW1iZWRkaW5ncyB0aGF0Cm9wdGltaXplZCBmb3IgdGhlIGNsYXNzaWZpY2F0aW9uIHByb2NlZHVyZSBhdCBoYW5kLgoKYGBge3J9CnNpbWlsYXJfY2xhc3NpZmljYXRpb25fd29yZHMoImhvcnJpYmxlIiwgZW1iZWRkaW5nX3d0cykKYGBgCiAKSGVyZSdzIGEgaGFuZHkgc2VxdWVuY2Ugb2YgY29kZSB0aGF0IHVzZXMgdGhlIFt0LVNORV0oaHR0cHM6Ly9iaXQubHkvMnJEazZycykgCm1ldGhvZG9sb2d5IHRvIHZpc3VhbGl6ZSBuZWFyZXN0IG5laWdoYm9yIHdvcmQgZW1iZWRkaW5ncy4KCmBgYHtyLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9Nn0KIyBwbG90dGluZyB0b28gbWFueSB3b3JkcyBtYWtlcyB0aGUgb3V0cHV0IGhhcmQgdG8gcmVhZApuX3dvcmRzX3RvX3Bsb3QgPC0gMTAwMAoKdHNuZSA8LSBSdHNuZTo6UnRzbmUoCiAgWCA9IGVtYmVkZGluZ193dHNbMTpuX3dvcmRzX3RvX3Bsb3QsXSwgCiAgcGVycGxleGl0eSA9IDEwMCwgCiAgcGNhID0gRkFMU0UKICApCgpwIDwtIHRzbmUkWSAlPiUKICBhcy5kYXRhLmZyYW1lKCkgJT4lCiAgbXV0YXRlKHdvcmQgPSByb3cubmFtZXMoZW1iZWRkaW5nX3d0cylbMTpuX3dvcmRzX3RvX3Bsb3RdKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBWMSwgeSA9IFYyLCBsYWJlbCA9IHdvcmQpKSArIAogIGdlb21fdGV4dChzaXplID0gMykKCnBsb3RseTo6Z2dwbG90bHkocCkKYGBgCiAKIA==